package example.demo.service.impl;

//import example.demo.entity.KillGoods;
//import example.demo.mapper.KillGoodsMapper;

import example.demo.service.IKillGoodsService;
//import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import redis.clients.jedis.Jedis;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author xiao_rao
 * @since 2021-07-30
 */
@Service
@Slf4j

public class KillGoodsServiceImpl implements IKillGoodsService, InitializingBean {

    //    @Resource
//    KillGoodsMapper killGoodsMapper;
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    RedisTemplate redisTemplate;


    private final String stock_prefix_redis_key = "stock_";

    @Value("${rao.segment-count:4}")
    int segmentCount;

    ConcurrentHashMap<String, ConcurrentHashMap<String, Boolean>> segmentMapFlag;

    @Autowired
    RedissonClient redissonClient;

    //要做到动态改变的话，这个还是要放到一个配置bean里面，不要放在KillGoodsServiceImpl下面，依赖注入这个bean就好了，引用传递
    //就需要借助配置中心+@RefreshScope。调用一下refresh的url
    //配置成数组，不要使用list
    @Value("${rao.good-ids:1001:100,1002:200,1003:100}")
    public String[] goodIds;

    /**
     * 前端传过来的key是100的，先拼成javaMap的key
     * kkk100:{kkk100-1:true,kkk100-2:true,kkk100-2:true}
     * 里面的是redis-key
     * <p>
     * InitializingBean借助这个来去弄缓存预热
     * <? 写热点分离
     */
    @Override
    public boolean redisSegment(Integer killId) {
        String javaMapKey = stock_prefix_redis_key + killId;
        if (isStockEmpty(javaMapKey)) {
            log.info("======库存不足");
            return false;
        }
        List<String> stockKeys = getStockKeyByKillId(javaMapKey);
        String redisKey = stockKeys.get(new Random().nextInt(stockKeys.size()));
        RLock lock = redissonClient.getLock(redisKey + "lock");
        /*todo 这段代码，如果你做了缓存预热的话，就不要这些注释的代码。
               还有key的过期时间持久到秒杀活动时间，而且这里的预扣减，就是没有对db进行修改操作。中途查询db在加入到redis超卖现象很容易发生*/
//        if (!stringRedisTemplate.hasKey(redisKey)) {
//            lock.lock(2, TimeUnit.SECONDS);
//            try {
//                if (!stringRedisTemplate.hasKey(redisKey)) {
//                    int db = 100;
//                    //query-db
//                    for (int i = 1; i <= 4; i++) {
//                        stringRedisTemplate.opsForValue().set(javaMapKey + i, String.valueOf(db / 4));
//                    }
//                }
//            } catch (Exception e) {
//                log.error(e.getMessage());
//            } finally {
//                lock.unlock();
//            }
//        }
        lock.lock(5, TimeUnit.SECONDS);//这种方式是没有看门狗的
        try {
            //查询缓存
            String stockStr = stringRedisTemplate.opsForValue().get(redisKey);
            int stockNum = Integer.parseInt(stockStr);
            if (stockNum <= 0) {
                //设置没有了库存
                segmentMapFlag.get(javaMapKey).put(redisKey, Boolean.FALSE);
                //走其他
                return redisSegment(killId);
            } else {
                Long decrement = stringRedisTemplate.opsForValue().decrement(redisKey);
                if (decrement >= 0) {
                    log.info("抢到了----" + redisKey + "====" + decrement);
                    return true;
                }
            }
        } catch (Exception e) {
            log.error(e.getMessage());
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
        return false;
    }


    private boolean isStockEmpty(String killID) {
        for (Map.Entry<String, Boolean> entry : segmentMapFlag.get(killID).entrySet()) {
            if (entry.getValue()) {
                return false;
            }
        }
        return true;
    }

    private List<String> getStockKeyByKillId(String killId) {
        ConcurrentHashMap<String, Boolean> map = segmentMapFlag.get(killId);
        List<String> keys = new ArrayList<>(4);
        for (Map.Entry<String, Boolean> entry : map.entrySet()) {
            if (entry.getValue()) {
                keys.add(entry.getKey());
            }
        }
        return keys;
    }

    /**
     * rao.good-ids: 1111:100 , 2222:200 , 3333:50 , 4444:200
     * <p>
     * 1) "stock_22221"
     * 2) "stock_44442"
     * 3) "stock_11114"
     * 4) "stock_33332"
     * 5) "stock_22223"
     * 6) "stock_33333"
     * <p>
     * 缓存预热的工作
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        segmentMapFlag = new ConcurrentHashMap<>();
        for (String goodId : goodIds) {
            String[] split = goodId.split(":");
            //商品id--killID1001
            String goodsID = split[0].trim();
            //每个段的库存数--100
            String goodsStock = split[1].trim();
            ConcurrentHashMap<String, Boolean> map = new ConcurrentHashMap<>();
            for (int i = 1; i <= segmentCount; i++) {
                //stock_10001_1 stock_10001_2 stock_10001_3...
                String stockKey = stock_prefix_redis_key + goodsID + i;
                //缓存预热
                stringRedisTemplate.opsForValue().setIfAbsent(stockKey, goodsStock, 30, TimeUnit.MINUTES);
                map.put(stockKey, Boolean.TRUE);
            }
            segmentMapFlag.put(stock_prefix_redis_key + goodsID, map);
        }
    }

    /**
     * 秒杀lua脚本
     * -3代表没有这个redis-key,就需要query-db
     * -2代表库存不足
     */
    public static final String second_kill_lua =
            // then...end
            "if redis.call('exists',KEYS[1])==1 then\n" +
                    "   local stock=tonumber(redis.call('get',KEYS[1]));\n" +
                    "   local num=tonumber(ARGV[1]);\n" +
                    "   if(stock>=num) then\n" +
                    "       return redis.call('decrby',KEYS[1],num);\n" +
                    "   end;\n" +
                    "   return -2;\n" +
                    "end;\n" +
                    "return -3;\n";//走了then...end就不会走下面的return
    ;


    @PostConstruct
    public void removeKey() {
        stringRedisTemplate.delete("lua10");
    }

    /**
     * 使用jedis替换了lettuce的依赖，所以从redis-template拿到的是jedis
     * 如果做了缓存预热的话，都不会进入同步代码块
     */
    @Override
    @SuppressWarnings("all")
    public boolean redisLua(Integer killId) {
        String redisKey = "lua" + killId;
        final byte[] argvBytes = "1".getBytes();
        final byte[] redisKeyBytes = redisKey.getBytes();
        Long result = (Long) stringRedisTemplate.execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
                return redisConnection.eval(second_kill_lua.getBytes(), ReturnType.INTEGER, 1, redisKeyBytes, argvBytes);
            }
        });
        //只有去查询db的线程才可以再去执行lua脚本的
        boolean flag = false;
        if (result == -3) {
            RLock lock = redissonClient.getLock("init-lock");
            lock.lock(5, TimeUnit.SECONDS);
            try {
                result = (Long) stringRedisTemplate.execute(new RedisCallback<Long>() {
                    @Override
                    public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
                        return redisConnection.eval(second_kill_lua.getBytes(), ReturnType.INTEGER, 1, redisKeyBytes, argvBytes);
                    }
                });
                if (result == -3) {
                    //get data form db only one thread just do
                    List<Integer> query = jdbcTemplate.query("select seckill_stock from kill_goods where id=?", new Integer[]{killId}, new RowMapper<Integer>() {
                        @Override
                        public Integer mapRow(ResultSet resultSet, int i) throws SQLException {//i是0，而数据库里面seckill_stock所在位置是1
//                            return resultSet.getInt(i);//getInt(idx) idx是数据库表对应的列。
                            return resultSet.getInt("seckill_stock");
                        }
                    });
                    //缓存query-db的结果到redis
                    stringRedisTemplate.opsForValue().set(redisKey, String.valueOf(query.get(0)));
                    flag = true;
                    //todo 不应该把下面的代码的放到redis分布式锁里面，lua脚本本身就是原子性的
//                    result = (Long) stringRedisTemplate.execute(new RedisCallback<Long>() {
//                        @Override
//                        public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
//                            return redisConnection.eval(second_kill_lua.getBytes(), ReturnType.INTEGER, 1, redisKey.getBytes(), "1".getBytes());
//                        }
//                    });
                }
            } catch (Exception exception) {
                log.error(exception.getMessage());
            } finally {
                if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
            if (flag) {
                result = (Long) stringRedisTemplate.execute(new RedisCallback<Long>() {
                    @Override
                    public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
                        return redisConnection.eval(second_kill_lua.getBytes(), ReturnType.INTEGER, 1, redisKeyBytes, argvBytes);
                    }
                });
            }
        }
        if (result != -2) {
            log.info("-----秒杀-succeed" + "---编号---" + (result + 1));
            return true;
        }
        log.info("-----秒杀-fail");
        return false;
    }

    @Autowired
    JdbcTemplate jdbcTemplate;

    @SuppressWarnings("all")
    @Override
    @Transactional
    public boolean db(Integer killId) {
        return jdbcTemplate.update("update kill_goods set seckill_stock = seckill_stock - ? where id = ? and seckill_stock > ?", 1, killId, 0) == 1;
    }

    /**
     * todo 借助4个表,实现写热点分离
     */
    public final static List<String> sql;

    static {
        sql = new ArrayList<>(4);
        sql.add("update kill_goods1 set seckill_stock = seckill_stock - ? where id = ? and seckill_stock > ?");
        sql.add("update kill_goods2 set seckill_stock = seckill_stock - ? where id = ? and seckill_stock > ?");
        sql.add("update kill_goods3 set seckill_stock = seckill_stock - ? where id = ? and seckill_stock > ?");
        sql.add("update kill_goods4 set seckill_stock = seckill_stock - ? where id = ? and seckill_stock > ?");
    }

    //nextInt使用了原子类
    final Random random = new Random();

    @Override
    public boolean dbMultiTableSegment(Integer killId) {
        return jdbcTemplate.update(sql.get(random.nextInt(4)), 1, killId, 0) == 1;
    }

    @Override
    public boolean dbMultiRowsSegment(Integer killId) {
        return killGoodsService.dbSegment2LeveUp(killId);
    }

    @Override
    public boolean dbMultiRowsSegmentTryOne(Integer killId) {
        return killGoodsService.dbSegment2(killId);
    }

    @Override
    public boolean dbMultiRowsSegmentTryOneCycleUse(Integer killId) {
        return killGoodsService.dbSegment2CycleUse(killId);
    }

    @Override
    public boolean redisLock(Integer killId) {
        String redisKey = "num" + killId;
        RLock lock = redissonClient.getLock(redisKey + "lock");
        lock.lock(2, TimeUnit.SECONDS);
        try {
            if (Integer.parseInt(stringRedisTemplate.opsForValue().get(redisKey)) > 0) {
                Long decrement = stringRedisTemplate.opsForValue().decrement(redisKey);
                log.info("扣减库存---succeed" + decrement);
                return true;
            } else {
                log.info("无库存");
            }
        } finally {
            lock.unlock();
        }
        return false;
    }


    /**
     * todo 借助表的四行记录,考虑间隙锁?
     * killId
     * killId+4
     * killId+8
     * killId+12
     */
    public final static List<String> sql2;

    static {
        sql2 = new ArrayList<>(4);
        sql2.add("update kill_goods set seckill_stock = seckill_stock - ? where id = ? and seckill_stock > ?");
        sql2.add("update kill_goods set seckill_stock = seckill_stock - ? where id = ?+4 and seckill_stock > ?");
        sql2.add("update kill_goods set seckill_stock = seckill_stock - ? where id = ?+8 and seckill_stock > ?");
        sql2.add("update kill_goods set seckill_stock = seckill_stock - ? where id = ?+12 and seckill_stock > ?");
    }


    //    @Override 只会调用一次update
//    @Transactional(timeout = 5)
    public boolean dbSegment2(Integer killId) {
        return jdbcTemplate.update(sql2.get(random.nextInt(4)), 1, killId, 0) == 1;
    }

    final AtomicInteger atomicInteger = new AtomicInteger(0);

    //    @Transactional(timeout = 5)
    public boolean dbSegment2CycleUse(Integer killId) {
        return jdbcTemplate.update(sql2.get(atomicInteger.getAndIncrement() & 3), 1, killId, 0) == 1;
    }

    //避免事务实现,自动注入自己
    @Autowired
    KillGoodsServiceImpl killGoodsService;

    public boolean dbSegment2LeveUp(Integer killId) {
        return killGoodsService.dbSegment2LeveUp(killId, random.nextInt(4), 0);
    }


    //    @Override 最多调用4次update
//    @Transactional(/*propagation = Propagation.NESTED*/ timeout = 3)
    public boolean dbSegment2LeveUp(Integer killId, int i, int count) {
        if (count == 4) {
            return false;
        }
        // 不使用 @Transactional,就不会爆这个错误[不过是在500个线程抢少数就会有这样的问题 ] Deadlock found when trying to get lock,
        boolean isSucceed = jdbcTemplate.update(sql2.get(i & 3), 1, killId, 0) == 1;
        if (!isSucceed) {
            return dbSegment2LeveUp(killId, ++i, ++count);
        } else {
            return true;
        }
    }
}
